package domein;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import javax.swing.JScrollBar;
import view.ServerForm;

/**
 * @author Colin Bundervoet
 * @author Jeroen Pattyn
 * @author Bram Schotte
 * @author Lucas Van Overberghe
 *
 * Server Object, holds a private inner class for connections.
 */
public class Server {

    private int port;
    private ServerForm form;
    private List<ClientThread> clients;
    private boolean running;
    private Set<String> operators;
    private Set<String> autoKicked;
    private Set<String> ipBanned;
    private String topic;
    private ServerSocket serverSocket;

    /**
     * Default constructor for creating a Server object with specific port
     * number.
     *
     * @param port the server should listen on
     */
    public Server(int port) {
        this.port = port;
    }

    /**
     * provides the form for which the Server should provide
     *
     * @param form
     */
    public void setForm(ServerForm form) {
        this.form = form;
    }

    /**
     * Returns the running state of the server as a boolean
     *
     * @return boolean whether the server is running
     */
    public boolean isRunning() {
        return running;
    }

    /**
     * Starts the server, initializing all components. Starting is not invoked
     * in the constructor and must be done separately. Always invoke this method
     * within a separate thread. Whichever thread it is invoked in will be
     * blocked, including ui threads.
     */
    public void start() {
        clients = new ArrayList<>();

        try {
            serverSocket = new ServerSocket(port);
            running = true;
            operators = fileReader("ops.txt");
            autoKicked = fileReader("kicked.txt");
            ipBanned = fileReader("banned.txt");
            topic = "";
            while (running) {
                showOnLog("waiting for connections...");

                Socket clientSocket = running ? serverSocket.accept() : null;
                if (ipBanned.contains(clientSocket.getInetAddress().toString())) {
                    showOnLog(clientSocket.getInetAddress().toString()
                            + " tried to connect.");
                    PrintWriter writer =
                            new PrintWriter(clientSocket.getOutputStream());
                    writer.println("You are banned.");
                    writer.flush();
                    writer.close();
                    clientSocket.close();
                } else {
                    PrintWriter writer =
                            new PrintWriter(clientSocket.getOutputStream());
                    ClientThread client =
                            new ClientThread(clientSocket, writer);
                    clients.add(client);

                    Thread listener = new Thread(client);
                    listener.start();

                    showOnLog(clientSocket.getInetAddress().toString()
                            + " connected.");
                }
            }
        } catch (Exception ex) {
            if (running) {
                showOnLog("Server has crashed, please restart");
            } else {
                showOnLog("Server shut down");
            }
            disconnectAllUsers();

        }
    }

    /**
     * Will read the file and loads all it's members into a TreeSet. Each entry
     * in the set will be taken from every newline in the file. If the file
     * doesn't exist the file will be created. method is invoked when starting
     * the server or forcing the /refresh command from a user.
     *
     * @param filename containing the data to be read.
     * @return Set containing all string representations from the file.
     */
    private Set<String> fileReader(String filename) {
        Set<String> users = new TreeSet<>();
        BufferedReader in;
        try {
            in = new BufferedReader(new FileReader(filename));
            while (in.ready()) {
                users.add(in.readLine());
            }

            in.close();
            showOnLog("Reading " + filename + " complete.");
        } catch (FileNotFoundException ex) {
            fileWriter(filename, users);
            showOnLog("File " + filename + " not found, creating...");
        } catch (IOException ex) {
            showOnLog("File " + filename + " could not be read");
        }
        return users;
    }

    /**
     * Creates a new file with parameter filename and persists all the data.
     * This method should only be invoked by the saveFiles() method.
     *
     * @param filename of the file to be written to.
     * @param users Set of characters to be written to the file.
     */
    private void fileWriter(String filename, Set<String> users) {
        PrintWriter out;

        try {
            out = new PrintWriter(new FileWriter(new File(filename)));
            for (String user : users) {
                out.println(user);
            }
            out.close();
            showOnLog("Writing " + filename + " complete.");
        } catch (IOException ex) {
            showOnLog("Error writing to " + filename);
        }
    }

    /**
     * saves each of the sets for operators, autokicked members and banned ips
     * This method should be invoked whenever changing any of these sets. To
     * remain consistent after possible server resets and restarts.
     */
    private void saveFiles() {
        fileWriter("ops.txt", operators);
        fileWriter("kicked.txt", autoKicked);
        fileWriter("banned.txt", ipBanned);
    }

    /**
     * Stops the server and disconnects all the members. Should be invoked
     * before trying to set this object to null.
     */
    public void stop() {
        running = false;
        try {
            serverSocket.close();
        } catch (IOException ex) {
            showOnLog("error trying to close socket.");
        }
        disconnectAllUsers();
    }

    /**
     * Will print the logged message to the server log view on the GUI. Using
     * System.out should be avoided and instead all print calls should be
     * handled by this method instead.
     *
     * @param logmessage
     */
    public void showOnLog(String logmessage) {
        SimpleDateFormat date = new SimpleDateFormat();
        String time = date.format(new Date());
        form.getServerLogArea().append(time + " " + logmessage + "\n");
        JScrollBar s = form.getjScrollPane1().getVerticalScrollBar();
        s.setValue(s.getMaximum());
    }

    /**
     * Sends a revised userlist to all the connected users. Should only be
     * called whenever the userlist changed. ie, during changes in nickname,
     * kicked members, leaving members, or timed out members. Default invoked
     * when a writer invokes its's close() method.
     */
    public synchronized void updateUserLists() {
        List<String> users = getConnectedUsers();
        Collections.sort(users);
        String msg = "¥userlist";
        for (String user : users) {
            msg += "¤" + user;
        }
        try {
            sendToAll(msg);
        } catch (Exception ex) {
            showOnLog("error telling everyone");
        }
        form.getOnlineUsersList().setListData(users.toArray());
        System.gc();
    }

    /**
     * Sends a raw message to all connected users.
     *
     * @param message String message to be sent to all users.
     */
    private synchronized void sendToAll(String message) {
        Iterator<ClientThread> it = clients.iterator();

        while (it.hasNext()) {
            try {
                ClientThread client = it.next();
                client.sendMessage(message);
            } catch (Exception ex) {
                showOnLog("error telling everyone");
            }
        }
    }

    /**
     * Will send a users online list to whoever invokes the specific request.
     * the provided writer's outputstream will be used to send the message. This
     * method call is meant for non-GUI updating list view.
     *
     * @param writer invokee of the function to return list to.
     */
    private void whoIsOnline(ClientThread client) {
        ArrayList<String> users = new ArrayList<>();
        Iterator<ClientThread> it = clients.iterator();

        while (it.hasNext()) {
            try {
                users.add(it.next().username);
            } catch (Exception ex) {
                showOnLog("Error grabbing usernames.");
            }
        }

        String message = "Currently online: ";

        for (String user : users) {
            message += "\n- " + user;
        }

        client.sendMessage(message);
        updateUserLists();
    }

    /**
     * Finds the ClientThread object with the provided username. Username is a
     * unique value for which to find a specific ClientThread. Should only be
     * invoked if trying to write to a specific outputstream which does not the
     * current user's stream or using the multicast option.
     *
     * @param username of the ClientThread to be found. Username is unique.
     * @return ClientThread object associated with specific username. returns
     * null in case of not found.
     */
    private ClientThread getUser(String username) {
        ClientThread ret = null;

        if (clients != null) {
            Iterator<ClientThread> it = clients.iterator();

            while (it.hasNext()) {
                try {
                    ClientThread t = it.next();
                    if (username.equals(t.username)) {
                        ret = t;
                    }

                } catch (Exception ex) {
                    showOnLog("Error grabbing username " + username);
                }
            }
        }
        return ret;

    }

    /**
     * Returns a String representation of all currently connected users in no
     * particular order. Implemented for ease of manipulation by in other
     * components.
     *
     * @return Array
     */
    private List<String> getConnectedUsers() {
        List<String> users = new ArrayList<>();

        if (clients != null) {
            Iterator<ClientThread> it = clients.iterator();

            while (it.hasNext()) {
                try {
                    users.add(it.next().username);
                } catch (Exception ex) {
                    showOnLog("Error grabbing usernames.");
                }
            }
        }

        return users;
    }

    /**
     * Disconnects all currently connected users, should be invoked when
     * stopping the current server process.
     */
    private void disconnectAllUsers() {
        Iterator<ClientThread> it = clients.iterator();

        while (it.hasNext()) {
            ClientThread t = it.next();
            t.close();
        }
    }

    /**
     * kicks the selected user from the server. Closing the connection and
     * notifying all connected members.
     *
     * @param username
     */
    public void kick(String username) {
        ClientThread ct = getUser(username);
        if (ct != null) {
            sendToAll(ct.username + " was kicked.");
            showOnLog(ct.username + " was kicked.");
            ct.close();
        } else {
            showOnLog("No such user " + username);
        }
    }

    /**
     * Adds connected user to ban list and kicks that user. All new connections
     * by this user will not be accepted.
     *
     * @param username
     * @return boolean to notify success or failure for any stream components.
     */
    public boolean ban(String username) {
        ClientThread ct = getUser(username);

        if (ct != null) {
            ipBanned.add(ct.sock.getInetAddress().toString());
            showOnLog(ct.sock.getInetAddress().toString() + " was banned");
            kick(username);
        }
        saveFiles();

        return ct != null;
    }

    /**
     * Sets mode operator on specified username. If user is already op nothing
     * will change. Can be invoked for unconnected users, will
     *
     * @param username user to be made operator.
     */
    public void op(String username) {
        operators.add(username);
        saveFiles();
        ClientThread ct = getUser(username);
        if (ct != null) {
            ct.sendMessage("You are now op.");
        }
        sendToAll("Setting mode operator on " + username);
    }

    /**
     * Removes mode operator from specified username. Can be invoked on
     * non-operator members, will not change their status. Can be invoked for
     * unconnected members, will change their status if it was operator.
     *
     * @param username user to be made regular user
     */
    public void deop(String username) {
        operators.remove(username);
        saveFiles();
        ClientThread ct = getUser(username);
        if (ct != null) {
            ct.sendMessage("You are no longer op.");

        }
        sendToAll("Removing mode operator on " + username);
    }

    /**
     * Looks up the whois innformation for a specific user.
     *
     * @param username user whose information is looked up
     * @return string containing information.
     */
    public String whois(String username) {
        ClientThread ct = getUser(username);

        if (ct != null) {
            return ct.username + "'s IP address is " + ct.sock.getInetAddress().toString();
        } else {
            return username + " could not be found.";
        }
    }

    /**
     * innerclass for ClientThreads. Every new ClientThread represents a single
     * connected user.
     */
    private class ClientThread implements Runnable {

        private BufferedReader reader;
        private Socket sock;
        private PrintWriter writer;
        protected String username;
        private boolean firstConnect;

        /**
         * Constructor for creating a new ClientThread. Requires clientsocket to
         * listen on, and the writer to write on.
         *
         * @param clientSocket Socket object to listen in on a specific
         * connection.
         * @param user PrintWriter object to which this writer needs to write
         * it's output
         */
        public ClientThread(Socket clientSocket, PrintWriter user) {
            writer = user;
            firstConnect = true;
            try {
                sock = clientSocket;
                InputStreamReader isReader =
                        new InputStreamReader(sock.getInputStream());
                reader = new BufferedReader(isReader);
            } catch (Exception ex) {
                showOnLog("error beginning StreamReader");
            }


        }

        /**
         * Changes the user's nickname to the specified nickname. notifies all
         * other users of this change. if the connection happens for the first
         * time the rules for choosing is much stricter. If the current nickname
         * is on the autokick list, Choosing it at any point at runtime will
         * close your connection.
         *
         * @param nick new nickname to be used as unique identifier.
         */
        public void changeNick(String nick) {
            String regex = "[A-Za-z0*9]*";
            if (nick.matches(regex)) {
                if (getConnectedUsers().contains(nick)) {
                    sendMessage("Your username is already taken");
                    if (username == null) {
                        close();
                    }
                } else {
                    if (autoKicked.contains(nick)) {
                        sendMessage("username not allowed");
                        close();
                    }
                    if (!firstConnect) {
                        sendToAll("" + username + " has changed their nickname to " + nick);
                    }
                    username = nick;
                    updateUserLists();
                }
            } else {
                sendMessage("You username could not be changed");
            }

        }

        /**
         * Closes this stream and removes it from the main thread's list of
         * clients. Should be invoked whenever a user decides to disconnect or
         * when a is at any point removed from the current conversation.
         */
        public void close() {
            try {
                sendMessage("You were disconnected");
                writer.close();
                reader.close();
                sock.close();
                clients.remove(this);
                updateUserLists();
            } catch (IOException ex) {
                showOnLog(username + " could not be disconnected.");
            }
        }

        /**
         * provides main parsing of incoming data, data proceeded by "/" will be
         * parsed into the correct action. Please refer to the manual for
         * specific instructions. Other messages will be multicast properly.
         */
        @Override
        public void run() {
            String message;

            try {
                while ((message = reader.readLine()) != null) {
                    if (firstConnect) {
                        String[] command = message.split(" ");
                        changeNick(command[1]);
                        sendMessage("The topic is: \"" + topic + "\"");
                        sendToAll(username + " has connected.");
                        firstConnect = false;
                        updateUserLists();
                    } else {
                        ClientThread ct = null;
                        String sendMessage = null;
                        if (message.charAt(0) == '/') {
                            showOnLog(username + "'s command: " + message);
                            String[] command = message.split(" ");
                            switch (command[0]) {
                                case "/nick":
                                    changeNick(command[1]);
                                    break;
                                case "/who":
                                    whoIsOnline(this);
                                    break;
                                case "/me":
                                case "/say":
                                    try {
                                        sendMessage = "* " + username + " ";
                                        for (int i = 1; i < command.length; i++) {
                                            sendMessage += command[i] + " ";
                                        }
                                        sendToAll(sendMessage);
                                    } catch (IndexOutOfBoundsException ex) {
                                        sendMessage("Could not parse, invalid use of " + command[0]);
                                    }
                                    break;
                                case "/":
                                    sendMessage("Stuck?! Try the /help command...");
                                    break;
                                case "/help":
                                    getHelp();
                                    break;
                                case "/pm":
                                case "/w":
                                case "/whisper":
                                    try {
                                        sendMessage = "[" + username + " > " + command[1] + "] ";
                                        for (int i = 2; i < command.length; i++) {
                                            sendMessage += command[i] + " ";
                                        }
                                        ct = getUser(command[1]);
                                        if (ct != null) {
                                            ct.sendMessage(sendMessage);
                                            sendMessage(sendMessage);
                                        } else {
                                            sendMessage(command[1] + " is currently not online.");
                                        }
                                    } catch (IndexOutOfBoundsException ex) {
                                        sendMessage("Could not parse, invalid use of " + command[0]);
                                    }
                                    break;
                                case "/quit":
                                case "/disconnect":
                                case "/dc":
                                case "/leave":
                                    close();
                                    break;

                                case "/op":
                                    try {
                                        if (operators.contains(username)) {
                                            op(command[1]);
                                        } else {
                                            sendMessage("You are not op.");
                                        }
                                    } catch (IndexOutOfBoundsException ex) {
                                        sendMessage("Could not parse, invalid use of " + command[0]);
                                    }
                                    break;
                                case "/deop":
                                    try {

                                        if (operators.contains(username)) {
                                            deop(command[1]);
                                        } else {
                                            sendMessage("You are not op.");
                                        }
                                    } catch (IndexOutOfBoundsException ex) {
                                        sendMessage("Could not parse, invalid use of " + command[0]);
                                    }
                                    break;

                                case "/kick":
                                    try {
                                        if (operators.contains(username)) {
                                            kick(command[1]);
                                        } else {
                                            sendMessage("You are not op.");
                                        }
                                    } catch (IndexOutOfBoundsException ex) {
                                        sendMessage("Could not parse, invalid use of " + command[0]);
                                    }
                                    break;
                                case "/autokick":
                                    try {


                                        if (operators.contains(username)) {
                                            autoKicked.add(command[1]);

                                            kick(command[1]);
                                            saveFiles();
                                        } else {
                                            sendMessage("You are not op.");
                                        }
                                    } catch (IndexOutOfBoundsException ex) {
                                        sendMessage("Could not parse, invalid use of " + command[0]);
                                    }
                                    break;
                                case "/refresh":
                                    if (operators.contains(username)) {
                                        ipBanned = fileReader("banned.txt");
                                        autoKicked = fileReader("kicked.txt");
                                        operators = fileReader("ops.txt");
                                        updateUserLists();
                                        sendMessage("Refreshing files complete");
                                    } else {
                                        sendMessage("You are not op.");
                                    }
                                    break;
                                case "/topic":
                                    try {

                                        if (operators.contains(username)) {
                                            sendMessage = username + " has changed topic to \"";
                                            String newtopic = "";
                                            for (int i = 1; i < command.length; i++) {
                                                sendMessage += command[i] + " ";
                                                newtopic += command[i] + " ";
                                            }
                                            sendMessage += "\"";
                                            topic = newtopic.trim();
                                            sendToAll(sendMessage.trim());
                                        } else {
                                            sendMessage("You are not op.");
                                        }
                                    } catch (IndexOutOfBoundsException ex) {
                                        sendMessage("Could not parse, invalid use of " + command[0]);
                                    }
                                    break;
                                case "/ipban":
                                case "/ban":
                                    try {
                                        if (operators.contains(username)) {
                                            if (!ban(command[1])) {
                                                sendMessage("User not online");
                                            }
                                        } else {
                                            sendMessage("You are not op.");
                                        }
                                    } catch (IndexOutOfBoundsException ex) {
                                        sendMessage("Could not parse, invalid use of " + command[0]);
                                    }
                                    break;

                                case "/ip":
                                    try {
                                        ct = getUser(command[1]);

                                        if (ct != null) {
                                            sendMessage(ct.sock.getInetAddress().toString());
                                        }
                                    } catch (IndexOutOfBoundsException ex) {
                                        sendMessage("Could not parse, invalid use of " + command[0]);
                                    }
                                    break;
                                case "/whois":
                                    try {
                                        ct = getUser(command[1]);

                                        if (ct != null) {
                                            sendMessage(ct.username + "'s IP address is " + ct.sock.getInetAddress().toString());
                                        }
                                    } catch (IndexOutOfBoundsException ex) {
                                        sendMessage("Could not parse, invalid use of " + command[0]);
                                    }
                                    break;
                                case "/getrole":
                                    try {
                                        if (operators.contains(command[1])) {
                                            sendMessage(command[1] + " is op.");
                                        } else {
                                            sendMessage(command[1] + " is a user.");
                                        }
                                    } catch (IndexOutOfBoundsException ex) {
                                        if (operators.contains(username)) {
                                            sendMessage("You are op.");
                                        } else {
                                            sendMessage("You are a user.");
                                        }
                                    }
                                    break;
                                default:
                                    sendMessage("Could not parse command.\n" + command[0], username + " tried to send command " + message);
                                    break;
                            }
                        } else {
                            String[] lines = message.split("\n");
                            if (message.trim().length() == 0) {
                                showOnLog("<" + username + "> " + "");
                            } else {
                                showOnLog("<" + username + "> " + message);
                                sendToAll("<" + username + "> " + message);
                            }
                        }
                    }


                }
            } catch (Exception ex) {
                showOnLog("Connection of " + username + " on " + sock.getInetAddress() + " dropped");
                clients.remove(this);
                updateUserLists();
            }
        }

        /**
         * Sends a message over the network to this specific user. Only invoke
         * this method to send messages to specific clients.
         *
         * @param message any string message.
         */
        public void sendMessage(String message) {
            writer.println(message);
            writer.flush();
        }

        /**
         * Sends a message with a corresponding logged message in one method
         * call. overload for sendMessage(String message)
         *
         * @param message any string message
         * @param logMessage message to be print on the server log.
         */
        public void sendMessage(String message, String logMessage) {
            sendMessage(message);
            showOnLog(logMessage);
        }

        /**
         * Returns the help message to the user.
         */
        public void getHelp() {
            sendMessage("So, let me help you out\n"
                    + "1. To send a message, just write it and click the send button\n"
                    + "2. Want to change your nickname? Try /nick\n"
                    + "3. /who to see all the connected users\n"
                    + "4. ");
        }
    }
}
